DMMF: 依存関係の注入
from Implementation: Composing a Pipeline
LT;DR
FP で 依存性の注入 を実現する方法
リーダモナド
フリーモナド
https://zenn.dev/zecl/books/d535208d9f6176
すべての依存関係をトップレベルの関数に渡して、バケツリレー する
本書はこれ
トップレベルの関数のことを、OOP にならって コンポジションルート と呼ぶ
コンポジションルート は エントリポイントと近いほうが良い
依存関係が多い場合、その原因のほとんどは関数の責務が多すぎることである
対応策
小さく分割する
上記が出来ない場合は、低レベルの関数をトップレベルの関数の外側でセットし、すべての依存関係が組み込まれている関数を渡す ようにすれば良い
DMMF: 依存関係の注入#66aa1d6c75d04f00005886c1
このように、ある関数が別の関数に渡されるときの インタフェース(関数の型)は、すべての依存関係を隠し、できるだけ小さくすべきである
hr.icon
関数型で『依存性の注入』を実現する
トップレベルから 依存関係 を必要とする関数まで、どのように依存関係を渡すか
依存関係 を必要とする関数
e.g. toProductCode : 最初のステップの実装#66a8ae9c75d04f0000c06b27
実現方法
OOP の場合
依存性の注入 や IoC コンテナ を使う
FP の場合
依存関係が暗黙的になるのは好ましくないので、依存関係を明示的なパラメータとして渡す
これを実現する方法は、リーダモナド や フリーモナド など色々ある
DMMF では、もっとも単純な方法にこだわる
もっとも単純な方法
すべての依存関係をトップレベルの関数に渡して、バケツリレー する
props drilling のように radish-miyazaki.icon
トップレベルから 依存関係 をバケツリレーする
e.g. パイプラインのステップを 1 つに合成する
トップレベル(コンポジションルート): ワークフロー 全体
code:fsharp
let placeOrder
checkProductCodeExists
checkAddressExists
getProductPrice
createOrderAcknowledgmentLetter
sendOrderAcknowledgment
: PlaceOrderWorkflow =
fun unvalidatedOrder ->
// ...
各ステップ
code:fsharp
let validateOrder: ValidateOrder =
fun checkProductCodeExists
checkAddressExists
unvalidatedOrder ->
let shippingAddress =
unvalidatedOrder.ShippingAddress
|> toAddress checkAddressExists
// ...
let lines =
unvalidatedOrder.Lines
|> List.map (toValidatedOrderLine checkProductCodeExists)
ヘルパ関数
code:fsharp
let toValidatedOrderLine checkProductExists unvalidatedOrderLine =
let orderLineId = ...
let productCode =
unvalidatedOrderLine.ProductCode
|> toProductCode checkProductExists
コンポジションルート の関数は、アプリケーションのエントリポイントにできるだけ近いものにすべき
メインコンポーネント
e.g. Suave を用いた Web アプリケーション
code:fsharp
let app: WebPart =
// ワークフローで使用するサービスの設定
let checkProductCodeExists = ...
let checkAddressExists = ...
let getProductPrice = ...
let createOrderAcknowledgmentLetter = ...
let sendOrderAcknowledgment = ...
let toHttpResponse = ...
// サービスの部分適用による placeOrder ワークフローの設定
let placeOrder =
placeOrder
checkProductCodeExists
checkAddressExists
getProductPrice
createOrderAcknowledgmentLetter
sendOrderAcknowledgment
// 他のワークフローの設定
let changeOrder = ...
let cancelOrder = ...
// ルーティングの設定
choose
[ POST >=> choose
[ path "/placeOrder"
=> deserializeOrder // JSON を未検証の注文に変換
=> placeOrder // ワークフローを実行
=> postEvents // イベントをキューに投入
=> toHttpResponse // 出力に基づきレスポンスを返す
path "/changeOrder"
=> ...
path "/cancelOrder"
=> ...
]
]
デシリアライズ については、DMMF: Serialization を参照
依存関係が多すぎる場合にすべきこと
依存関係 が多すぎる場合、関数が行っていることが多すぎる可能性がある
小さい要素に分割できないか?
難しい場合は、依存関係を 1 つのレコード構造にまとめる
1 つのパラメータとして渡すことも考えられる
よくあるのが、子関数の依存関係が複雑になっているケース
e.g. checkAddressExists 関数が URI エンドポイントと認証情報を必要とする Web サービスと通信している
code:fsharp
let checkAddressExists endPoint crendentials =
....
toAddress の呼び出しにも、これらの 2 つのパラメータを渡す必要がある?
code:fsharp
let toAddress checkAddressExists endPoint crendentials unvalidatedAddress =
...
let checkAddress = checkAddressExists endPoint crendentials unvalidatedAddress
toAddress の呼び出し元も、その呼び出し元も渡す必要が出てくる
code:fsharp
let validateOrder
checkProductCodeExists
checkAddressExists
endPoint
credentials
unvalidatedOrder =
...
...
代替案
低レベルの関数をトップレベルの関数の外側でセットし、すべての依存関係が組み込まれている関数を渡す
code:fsharp
let placeOrder: PlaceOrderWorkflow =
let endPoint = ...
let credentials = ...
// 認証情報を埋め込んだ checkAddressExists の新しいバージョンを作成
let checkAddressExists = checkAddressExists endPoint credentials
// ワークフローのステップを設定
let validateOrder =
validateOrder checkProductCodeExists checkAddressExists
...
// ワークフロー関数を返す
fun unvalidatedOrder ->
unvalidatedOrder
|> validateOrder
// ...
ある関数が別の関数に渡されるときの インタフェース(関数の型)は、すべての依存関係を隠し、できるだけ小さくすべきである